/*
 * Hosted Game Core
 * D1st0rt, SSCE Hyperspace
 * License: MIT/X11
 * Created: 2009-01-14
 *
 * Layer of abstraction that provides common functionality for games
 * having a human host. Games which desire use of the core should
 * implement the Ihostedgame interface and attach to both the arena and
 * this module. Only one Ihostedgame can be attached per arena.
 */

///////////////////////////////////////////////////////////////////////
// Includes
///////////////////////////////////////////////////////////////////////

#include "stdio.h"
#include "string.h"
#include "asss.h"
#include "persist.h"
#include "hgcore.h"

///////////////////////////////////////////////////////////////////////
// Interfaces
///////////////////////////////////////////////////////////////////////

local Imodman *mm;
local Ilogman *lm;
local Ichat *chat;
local Igame *game;
local Icmdman *cmd;
local Iarenaman *aman;
local Iplayerdata *pd;
local Ipersist *persist;
local Iconfig *config;
local Icapman *capman;

///////////////////////////////////////////////////////////////////////
// Defines/Types
///////////////////////////////////////////////////////////////////////

typedef struct hgcore_adata
{
    int Status;
    LinkedList *Teams;
    HashTable *Players;
    Ihostedgame *hg;
    char AttachedGame[32];
    int SizeOfExtraData;
    int cfg_ElimOnSpec;
    int cfg_AutoTeamSize;
    char *cfg_AllowedGames;
} hgcore_adata;

#define HG_CHECK(a) if(!a) return; hgcore_adata *ad = P_ARENA_DATA(a, adkey); if(!ad || !ad->hg) return
#define HG_CHECK_RETURN(a,r) if(!a) return r; hgcore_adata *ad = P_ARENA_DATA(a, adkey); if(!ad || !ad->hg) return r

///////////////////////////////////////////////////////////////////////
// Globals
///////////////////////////////////////////////////////////////////////
EXPORT const char info_hgcore[] = "pre_09.05.12 by D1st0rt <d1st0rter@gmail.com>";

local pthread_mutex_t globalmutex;
local int adkey;

///////////////////////////////////////////////////////////////////////
// Interface Prototypes
///////////////////////////////////////////////////////////////////////
local int AttachGame(Arena *a, const char *hgmod_name, const char *game_name);
local int DetachGame(Arena *a);
local void EndGame(Arena *a, int how);

local void AddIneligibleTeam(Arena *a, int freq);

local int PlayerStatus(Player *p, Arena *a);
local int TeamStatus(Arena *a, int freq);
local int GameStatus(Arena *a);

local void EliminatePlayer(Arena *a, Player *p);
local void EliminateTeam(Arena *a, int freq);
local int RemainingTeams(Arena *a);

local void *GetTeamData(Arena *a, int freq);
local void TeamEnum(Arena *a, HGTeamEnumFunc *func, void *data);

local void EndInterval(Arena *a, int interval);

///////////////////////////////////////////////////////////////////////
// Prototypes
///////////////////////////////////////////////////////////////////////
local int GameAllowed(Arena *a, const char *game);
local int HostAllowed(Arena *a, Player *host);

local void PrepareGame(Arena *a, Player *p, const char *params);
local void StartGame(Arena *a, Player *p);
local void CleanupGame(Arena *a);

local HGTeam *GetTeam(Arena *a, int freq);
local void AddTeam(Arena *a, int freq);
local void RemoveTeam(Arena *a, int freq);
local void FreeHGTeam(HGTeam *team);
local void ShowTeam(HGTeam *team, Player *p);
local void MakeAutoTeams(Arena *a, int size);

local void EliminateEmptyTeams(Arena *a);
local void CheckForGameOver(Arena *a);

///////////////////////////////////////////////////////////////////////
// Callbacks
///////////////////////////////////////////////////////////////////////
local void PlayerAction(Player *p, int action, Arena *a);
local void ShipChange(Player *p, int ship, int freq);
local void FreqChange(Player *p, int freq);
local void ArenaAction(Arena *a, int action);

///////////////////////////////////////////////////////////////////////
// Timers
///////////////////////////////////////////////////////////////////////


///////////////////////////////////////////////////////////////////////
// Commands
///////////////////////////////////////////////////////////////////////
local helptext_t showteams_help =
"Targets: none\n"
"Args: none\n"
"Shows teams participating in game.";
local void C_showteams(const char *command, const char *params, Player *p, const Target *target);

local helptext_t addteam_help =
"Targets: freq\n"
"Args: none\n"
"Adds a team to the game.";
local void C_addteam(const char *command, const char *params, Player *p, const Target *target);

local helptext_t removeteam_help =
"Targets: freq\n"
"Args: none\n"
"Removes a team from the game.";
local void C_removeteam(const char *command, const char *params, Player *p, const Target *target);

local helptext_t newgame_help =
"Targets: none\n"
"Args: none\n"
"Sets up a game. Use ?addteam to add teams.";
local void C_newgame(const char *command, const char *params, Player *p, const Target *target);

local helptext_t startgame_help =
"Targets: none\n"
"Args: none\n"
"Once a game has been created and teams have been added, begins play.";
local void C_startgame(const char *command, const char *params, Player *p, const Target *target);

local helptext_t stopgame_help =
"Targets: none\n"
"Args: none\n"
"Stops any game currently in progress.";
local void C_stopgame(const char *command, const char *params, Player *p, const Target *target);

local helptext_t listgames_help =
"Targets: none\n"
"Args: none\n"
"Shows hosted games that the arena can be switched to.";
local void C_listgames(const char *command, const char *params, Player *p, const Target *target);

local helptext_t switchgame_help =
"Targets: none\n"
"Args: name of game module to switch to\n"
"Switches the arena to a specified game.";
local void C_switchgame(const char *command, const char *params, Player *p, const Target *target);

///////////////////////////////////////////////////////////////////////
// Entry Point
///////////////////////////////////////////////////////////////////////
local Ihgcore interface =
{
    INTERFACE_HEAD_INIT(I_HGCORE, "hgcore")
    AttachGame,
    DetachGame,
    EndGame,
    AddIneligibleTeam,
    PlayerStatus,
    TeamStatus,
    GameStatus,
    EliminatePlayer,
    EliminateTeam,
    RemainingTeams,
    EndInterval,
    TeamEnum,
    GetTeamData
};

EXPORT int MM_hgcore(int action, Imodman *mm_, Arena *arena)
{
    if (action == MM_LOAD)
    {
        // Global interfaces
        mm = mm_;
        chat    = mm->GetInterface(I_CHAT,       ALLARENAS);
        cmd     = mm->GetInterface(I_CMDMAN,     ALLARENAS);
        pd      = mm->GetInterface(I_PLAYERDATA, ALLARENAS);
        game    = mm->GetInterface(I_GAME,       ALLARENAS);
        aman    = mm->GetInterface(I_ARENAMAN,   ALLARENAS);
        lm      = mm->GetInterface(I_LOGMAN,     ALLARENAS);
        persist = mm->GetInterface(I_PERSIST,   ALLARENAS);
        config  = mm->GetInterface(I_CONFIG,     ALLARENAS);
        capman  = mm->GetInterface(I_CAPMAN,     ALLARENAS);

        if (!chat || !cmd || !pd || !game || !aman || !lm || !persist || !config || !capman)
            return MM_FAIL;

        // Data
        adkey = aman->AllocateArenaData(sizeof(hgcore_adata));
        if (adkey == -1) return MM_FAIL;

        // Locking
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        pthread_mutex_init(&globalmutex, &attr);
        pthread_mutexattr_destroy(&attr);

        // Interface registration
        mm->RegInterface(&interface, ALLARENAS);

        return MM_OK;
    }
    else if (action == MM_UNLOAD)
    {
        // Interface registration
        if (mm->UnregInterface(&interface, ALLARENAS))
        {
            return MM_FAIL;
        }

        // Locking
        pthread_mutex_destroy(&globalmutex);

        // Data
        aman->FreeArenaData(adkey);

        // Global interfaces
        mm->ReleaseInterface(capman);
        mm->ReleaseInterface(config);
        mm->ReleaseInterface(persist);
        mm->ReleaseInterface(lm);
        mm->ReleaseInterface(aman);
        mm->ReleaseInterface(game);
        mm->ReleaseInterface(pd);
        mm->ReleaseInterface(cmd);
        mm->ReleaseInterface(chat);

        return MM_OK;
    }

    else if (action == MM_ATTACH)
    {
        //Data
        hgcore_adata *ad = P_ARENA_DATA(arena, adkey);
        pthread_mutex_lock(&globalmutex);
        ad->Teams = LLAlloc();
        ad->Players = HashAlloc();
        pthread_mutex_unlock(&globalmutex);

        /* cfghelp: HostedGame:ElimOnSpec, arena, int, def: 1, mod: hgcore
         * Whether players are automatically eliminated when they spec. */
        ad->cfg_ElimOnSpec = config->GetInt(arena->cfg, "HostedGame", "ElimOnSpec", 1);

        /* cfghelp: HostedGame:AutoTeamSize, arena, int, def: 0, mod: hgcore
         * If size > 0, automatically makes teams of size at game start. */
        ad->cfg_AutoTeamSize = config->GetInt(arena->cfg, "HostedGame", "AutoTeamSize", 0);

        /* cfghelp: HostedGame:AllowedGames, arena, string
         * List of games (by module name) that can be hosted in this arena. */
        const char* games = config->GetStr(arena->cfg, "HostedGame", "AllowedGames");
        if (games)
        {
            int size = strlen(games) + 1;
            ad->cfg_AllowedGames = amalloc(size);
            astrncpy(ad->cfg_AllowedGames, games, size);
        }

        //Attached interfaces

        //Callbacks
        mm->RegCallback(CB_PLAYERACTION, PlayerAction, arena);
        mm->RegCallback(CB_SHIPCHANGE, ShipChange, arena);
        mm->RegCallback(CB_FREQCHANGE, FreqChange, arena);
        mm->RegCallback(CB_ARENAACTION, ArenaAction, arena);

        //Timers

        //Commands
        cmd->AddCommand("newgame", C_newgame, arena, newgame_help);
        cmd->AddCommand("addteam", C_addteam, arena, addteam_help);
        cmd->AddCommand("removeteam", C_removeteam, arena, removeteam_help);
        cmd->AddCommand("showteams", C_showteams, arena, showteams_help);
        cmd->AddCommand("startgame", C_startgame, arena, startgame_help);
        cmd->AddCommand("stopgame", C_stopgame, arena, stopgame_help);
        cmd->AddCommand("listgames", C_listgames, arena, listgames_help);
        cmd->AddCommand("switchgame", C_switchgame, arena, switchgame_help);

        return MM_OK;
    }
    else if (action == MM_DETACH)
    {
        //Data
        hgcore_adata *ad = P_ARENA_DATA(arena, adkey);
        if(ad->Status != HGS_IDLE)
        {
            CleanupGame(arena);
        }
        pthread_mutex_lock(&globalmutex);
        LLFree(ad->Teams);
        HashFree(ad->Players);
        pthread_mutex_unlock(&globalmutex);
        afree(ad->cfg_AllowedGames);

        //Timers

        //Callbacks
        mm->UnregCallback(CB_PLAYERACTION, PlayerAction, arena);
        mm->UnregCallback(CB_SHIPCHANGE, ShipChange, arena);
        mm->UnregCallback(CB_FREQCHANGE, FreqChange, arena);
        mm->UnregCallback(CB_ARENAACTION, ArenaAction, arena);

        //Commands
        cmd->RemoveCommand("newgame", C_newgame, arena);
        cmd->RemoveCommand("addteam", C_addteam, arena);
        cmd->RemoveCommand("removeteam", C_removeteam, arena);
        cmd->RemoveCommand("showteams", C_showteams, arena);
        cmd->RemoveCommand("startgame", C_startgame, arena);
        cmd->RemoveCommand("stopgame", C_stopgame, arena);
        cmd->RemoveCommand("listgames", C_listgames, arena);
        cmd->RemoveCommand("switchgame", C_switchgame, arena);
    }
    else if (action == MM_PREUNLOAD) //server is shutting down
    {
        Link *link;
        Arena *a;
        aman->Lock();
        FOR_EACH_ARENA(a)
        {
            EndGame(a, HGR_FORCED);
            DetachGame(a);
        }
        aman->Unlock();
    }

    return MM_FAIL;
}

///////////////////////////////////////////////////////////////////////
// Interface Functions
///////////////////////////////////////////////////////////////////////
local int AttachGame(Arena *a, const char *hgmod_name, const char *game_name)
{
    hgcore_adata *ad = P_ARENA_DATA(a, adkey);
    int success = 0;

    pthread_mutex_lock(&globalmutex);
    ad->hg = mm->GetInterface(hgmod_name, a);

    if(ad->hg)
    {
        ad->SizeOfExtraData = ad->hg->SizeOfTeamData();
        success = 1;
        astrncpy(ad->AttachedGame, game_name, 32);
        lm->LogA(L_INFO, "hgcore", a, "Attached hosted game %s.", game_name);
    }
    else
    {
        lm->LogA(L_ERROR, "hgcore", a, "Error attaching to hosted game %s.", game_name);
    }
    pthread_mutex_unlock(&globalmutex);

    return success;
}

local int DetachGame(Arena *a)
{
    HG_CHECK_RETURN(a, 0);
    int ret = 0;

    pthread_mutex_lock(&globalmutex);
    if(ad->Status == HGS_IDLE && ad->hg)
    {
        mm->ReleaseInterface(ad->hg);
        ad->hg = NULL;
        memset(ad->AttachedGame, 0, 32);
        lm->LogA(L_INFO, "hgcore", a, "Detached hosted game.");
        ret = 1;
    }
    else if(!ad->hg)
    {
        lm->LogA(L_WARN, "hgcore", a, "Ignored detaching hosted game that is already detached.");
    }
    else
    {
        lm->LogA(L_ERROR, "hgcore", a, "Cannot detach from hosted game, it is in progress.");
    }
    pthread_mutex_unlock(&globalmutex);

    return ret;
}

local void EndGame(Arena *a, int how)
{
    HG_CHECK(a);

    if(ad->Status != HGS_POSTGAME && ad->Status != HGS_IDLE)
    {
        lm->LogA(L_DRIVEL, "hgcore", a, "Game ending with status code %d", how);

        ad->Status = HGS_POSTGAME;
        Player *p;
        Link *link;
        HGTeam *team;

        pthread_mutex_lock(&globalmutex);

        link = LLGetHead(ad->Teams);
        while(link)
        {
            team = (HGTeam *)link->data;
            if(team->Status == HGS_ACTIVE)
            {
                team->Status = HGS_POSTGAME;
            }
            link = link->next;
        }

        pd->Lock();
        FOR_EACH_PLAYER(p)
        {
            if(p->arena == a && p->p_ship != SHIP_SPEC)
            {
                int *status = HashGetOne(ad->Players, p->name);
                if(status)
                {
                    if(*status == HGS_ACTIVE)
                    {
                        *status = HGS_POSTGAME;
                    }
                }
            }
        }
        pd->Unlock();

        pthread_mutex_unlock(&globalmutex);

        //Now tell game module
        ad->hg->OnGameOver(a, how);

        //Time to clean up!
        CleanupGame(a);
    }
    else
    {
        lm->LogA(L_WARN, "hgcore", a, "Ignored trying to end game that is already in POSTGAME or IDLE");
    }
}

local void AddIneligibleTeam(Arena *a, int freq)
{
    AddTeam(a, freq);
    HGTeam *team = GetTeam(a, freq);
    team->Status = HGS_IDLE;
}

local int PlayerStatus(Player *p, Arena *a)
{
    HG_CHECK_RETURN(a, -1);
    int ret = -1;

    pthread_mutex_lock(&globalmutex);
    int *status = (int*)HashGetOne(ad->Players, p->name);
    pthread_mutex_unlock(&globalmutex);

    if(status)
    {
        ret = *status;
    }

    return ret;
}

local int TeamStatus(Arena *a, int freq)
{
    HG_CHECK_RETURN(a, -1);
    int ret = -1;

    HGTeam *team = GetTeam(a, freq);
    if(team)
    {
        ret = team->Status;
    }

    return ret;
}

local int GameStatus(Arena *a)
{
    HG_CHECK_RETURN(a, -1);
    return ad->Status;
}

local void *GetTeamData(Arena *a, int freq)
{
    HGTeam *team = GetTeam(a, freq);

    if(!team)
    {
        lm->LogA(L_ERROR, "hgcore", a, "Tried to get data for nonexistant team %d.", freq);
        return NULL;
    }

    return team->Data;
}

local void EliminatePlayer(Arena *a, Player *p)
{
    HG_CHECK(a);

    if(ad->Status == HGS_ACTIVE)
    {
        pthread_mutex_lock(&globalmutex);
        int *status = HashGetOne(ad->Players, p->name);
        //is this player actually participating?
        if(status)
        {
            if(*status == HGS_ACTIVE || *status == HGS_PAUSED)
            {
                //ok, set their status
                *status = HGS_ELIMINATED;
                EliminateEmptyTeams(a);
                CheckForGameOver(a);
            }
        }
        pthread_mutex_unlock(&globalmutex);
    }
}

local void EliminateTeam(Arena *a, int freq)
{
    HG_CHECK(a);
    pthread_mutex_lock(&globalmutex);
    HGTeam *team = GetTeam(a, freq);
    if(team)
    {
        if(team->Status == HGS_ELIMINATED || team->Status == HGS_IDLE)
        {
            lm->LogA(L_WARN, "hgcore", a, "Asked to eliminate non-participant team %d", freq);
        }
        team->Status = HGS_ELIMINATED;
    }
    else
    {
        lm->LogA(L_ERROR, "hgcore", a, "Tried to eliminate nonexistant team %d.", freq);
    }
    pthread_mutex_unlock(&globalmutex);
    CheckForGameOver(a);
}

local int RemainingTeams(Arena *a)
{
    HG_CHECK_RETURN(a,-1);
    int count = 0;

    HGTeam *team;
    Link *link;

    pthread_mutex_lock(&globalmutex);
    for (link = LLGetHead(ad->Teams);
        link && ((team = link->data, link = link->next) || 1); )
    {
        if(team->Status != HGS_IDLE && team->Status != HGS_ELIMINATED)
        {
            count++;
        }
    }
    pthread_mutex_unlock(&globalmutex);

    return count;
}

local void EndInterval(Arena *a, int interval)
{
    persist->EndInterval(NULL, a, interval);
}

local void TeamEnum(Arena *a, HGTeamEnumFunc *func, void *data)
{
    if(!func) return;
    HG_CHECK(a);

    Link *link;
    HGTeam *team;

    pthread_mutex_lock(&globalmutex);
    for (link = LLGetHead(ad->Teams);
        link && ((team = link->data, link = link->next) || 1); )
    {
        if(ad->Status == HGS_IDLE)
            break;

        ((HGTeamEnumFunc)func)(team, data);
    }
    pthread_mutex_unlock(&globalmutex);
}

///////////////////////////////////////////////////////////////////////
// Functions
///////////////////////////////////////////////////////////////////////
local int GameAllowed(Arena *a, const char *name)
{
    hgcore_adata *ad = P_ARENA_DATA(a, adkey);
    if(!ad || !ad->cfg_AllowedGames) return 0;
    // do not fall back to full permissions if not defined, the only
    // time this is called is from the ?switchgame command

    char *tmp = strstr(ad->cfg_AllowedGames, name);
    if(tmp)
    {
        return strncmp(tmp, name, strlen(name)) == 0;
    }

    return 0;
}

local int HostAllowed(Arena *a, Player *host)
{
    hgcore_adata *ad = P_ARENA_DATA(a, adkey);
    if(!ad) return 0;

    const char *games = ad->cfg_AllowedGames;
    if(!games) return 1; //fall back to full permissions if not defined.

    char buf[40];
    sprintf(buf, "host_%s", ad->AttachedGame);
    return capman->HasCapability(host, buf);
}

local void PrepareGame(Arena *a, Player *host, const char *params)
{
    HG_CHECK(a);
    lm->LogA(L_DRIVEL, "hgcore", a, "Trying to prepare game for host %s", host->name);

    //first check with game module
    if(ad->hg->OnPrepare(a, host, params))
    {
        Player *p;
        Link *link;

        pd->Lock();
        FOR_EACH_PLAYER(p)
        {
            if(p->arena == a)
            {
                pthread_mutex_lock(&globalmutex);
                int *status = amalloc(sizeof(int));
                if(p->p_ship == SHIP_SPEC)
                {
                    *status = HGS_IDLE;
                }
                else
                {
                    *status = HGS_PREGAME;
                }
                HashReplace(ad->Players, p->name, status);
                pthread_mutex_unlock(&globalmutex);
            }
        }
        pd->Unlock();

        ad->Status = HGS_PREGAME;
        lm->LogA(L_DRIVEL, "hgcore", a, "Game prepared for host %s", host->name);
    }
    else
    {
        chat->SendMessage(host, "Failed to create new game.");
        lm->LogA(L_DRIVEL, "hgcore", a, "Couldn't prepare game for host %s", host->name);
    }
}

local void StartGame(Arena *a, Player *host)
{
    HG_CHECK(a);
    lm->LogA(L_DRIVEL, "hgcore", a, "Trying to start game for host %s", host->name);

    //first check with game module
    if(ad->hg->OnStart(a, host))
    {
        Player *p;
        Link *link;
        HGTeam *team;

        pd->Lock();
        FOR_EACH_PLAYER(p)
        {
            if(p->arena == a)
            {
                pthread_mutex_lock(&globalmutex);
                int *status = HashGetOne(ad->Players, p->name);
                if(!status) //this is probably a fake player
                {
                    status = amalloc(sizeof(int));
                    *status = HGS_PREGAME;
                    HashReplace(ad->Players, p->name, status);
                }

                if(*status == HGS_PREGAME && p->p_ship != SHIP_SPEC)
                {
                    *status = HGS_ACTIVE;
                }
                else
                {
                    *status = HGS_IDLE;
                }
                status = HashGetOne(ad->Players, p->name);
                pthread_mutex_unlock(&globalmutex);
            }
        }
        pd->Unlock();

        pthread_mutex_lock(&globalmutex);
        for (link = LLGetHead(ad->Teams);
        link && ((team = link->data, link = link->next) || 1); )
        {
            if(team->Status == HGS_PREGAME)
            {
                team->Status = HGS_ACTIVE;
            }
            else
            {
                lm->LogA(L_DRIVEL, "hgcore", a, "Freq %d is ineligible, be sure this is intended", team->Freq);
            }
        }
        pthread_mutex_unlock(&globalmutex);

        ad->Status = HGS_ACTIVE;
        lm->LogA(L_DRIVEL, "hgcore", a, "Game started for host %s", host->name);
    }
    else
    {
        chat->SendMessage(host, "Failed to start game.");
        lm->LogA(L_DRIVEL, "hgcore", a, "Couldn't start game for host %s", host->name);
    }
}

local void CleanupGame(Arena *a)
{
    HG_CHECK(a);

    lm->LogA(L_DRIVEL, "hgcore", a, "Cleaning up game.");
    ad->hg->OnCleanup(a);

    //Now do core cleanup
    pthread_mutex_lock(&globalmutex);
    LLEnum(ad->Teams, (void *)FreeHGTeam);
    LLEmpty(ad->Teams);
    pthread_mutex_unlock(&globalmutex);

    Link *link;
    Player *p;

    pd->Lock();
    FOR_EACH_PLAYER(p)
    {
        if(p->arena == a)
        {
            pthread_mutex_lock(&globalmutex);
            int *status = HashGetOne(ad->Players, p->name);
            HashRemove(ad->Players, p->name, status);
            afree(status);
            pthread_mutex_unlock(&globalmutex);
        }
    }
    pd->Unlock();

    ad->Status = HGS_IDLE;
}

local HGTeam *GetTeam(Arena *a, int freq)
{
    HG_CHECK_RETURN(a, NULL);

    Link *link;
    HGTeam *team;
    HGTeam *ret = NULL;

    pthread_mutex_lock(&globalmutex);
    for (link = LLGetHead(ad->Teams);
        link && ((team = link->data, link = link->next) || 1); )
    {
        if(team->Freq == freq)
        {
            ret = team;
            break;
        }
    }
    pthread_mutex_unlock(&globalmutex);

    return ret;
}

local void AddTeam(Arena *a, int freq)
{
    HG_CHECK(a);
    HGTeam *team = amalloc(sizeof(HGTeam));
    team->Arena = a;
    team->Freq = freq;
    team->Data = amalloc(ad->SizeOfExtraData);

    pthread_mutex_lock(&globalmutex);
    LLAdd(ad->Teams, team);
    pthread_mutex_unlock(&globalmutex);

    ad->hg->OnTeamCreate(a, team);
}

local void RemoveTeam(Arena *a, int freq)
{
    HG_CHECK(a);

    HGTeam *team = GetTeam(a, freq);
    if(team)
    {
        pthread_mutex_lock(&globalmutex);
        LLRemove(ad->Teams, team);
        pthread_mutex_unlock(&globalmutex);

        FreeHGTeam(team);
    }
}

local void FreeHGTeam(HGTeam *team)
{
    if(!team) return;
    HG_CHECK(team->Arena);

    ad->hg->OnTeamDestroy(team->Arena, team);

    afree(team->Data);
    afree(team);
}

local void ShowTeam(HGTeam *team, Player *t)
{
    if(!team) return;
    HG_CHECK(team->Arena);

    Player *p;
    Link *link;
    int size = 0;

    pd->Lock();
    FOR_EACH_PLAYER(p)
    {
        if(p->arena == team->Arena && p->p_freq == team->Freq)
        {
            int pstatus = PlayerStatus(p, team->Arena);
            if(pstatus != HGS_IDLE && pstatus != HGS_ELIMINATED)
            {
                size++;
            }
        }
    }
    pd->Unlock();

    char buf[250];
    sprintf(buf, "[%4d] Players: %d ", team->Freq, size);
    ad->hg->TeamInfoString(team, buf, sizeof(buf));

    char line[256], *c;
    const char *temp = NULL;
    while (strsplit(buf, "\n", line, 256, &temp))
    {
        for (c = line; *c; c++)
            if (*c == '{' || *c == '}')
                *c = '\'';
        chat->SendMessage(t, "%s", line);
    }
}

local void MakeAutoTeams(Arena *a, int size)
{
    HG_CHECK(a);

    if(ad->Status == HGS_PREGAME)
    {
        int count = 0;
        int freq = 0;
        Player *p;
        Link *link;

        pd->Lock();
        FOR_EACH_PLAYER(p)
        {
            if(p->arena == a)
            {
                //hack to cover fake players
                pthread_mutex_lock(&globalmutex);
                int *status = HashGetOne(ad->Players, p->name);
                if(!status)
                {
                    status = amalloc(sizeof(int));
                    *status = HGS_PREGAME;
                    HashReplace(ad->Players, p->name, status);
                }
                pthread_mutex_unlock(&globalmutex);

                if(PlayerStatus(p, a) == HGS_PREGAME)
                {
                    game->SetFreq(p, freq);
                    count++;
                    if(count % size == 0)
                    {
                        HGTeam *team = GetTeam(a, freq);
                        if(!team)
                        {
                            AddTeam(a, freq);
                            team = GetTeam(a, freq);
                            team->Status = HGS_PREGAME;
                        }
                        freq++;
                    }
                }
            }
        }
        pd->Unlock();

        lm->LogA(L_DRIVEL, "hgcore", a, "Created %d teams from %d players", freq, count);
    }
}

local void EliminateEmptyTeams(Arena *a)
{
    HG_CHECK(a);

    HGTeam *team;
    HashTable *teams;
    Link *link;
    Player *p;
    char buf[5];

    if(ad->Status == HGS_ACTIVE)
    {
        teams = HashAlloc();
        pd->Lock();
        FOR_EACH_PLAYER(p)
        {
            if(p->arena == a)
            {
                pthread_mutex_lock(&globalmutex);
                int *status = HashGetOne(ad->Players, p->name);
                if(*status == HGS_ACTIVE || *status == HGS_PAUSED)
                {
                    sprintf(buf, "%d", p->p_freq);
                    char *dummy = amalloc(sizeof(char));
                    HashReplace(teams, buf, dummy);
                }
                pthread_mutex_unlock(&globalmutex);
            }
        }
        pd->Unlock();

        pthread_mutex_lock(&globalmutex);
        link = LLGetHead(ad->Teams);
        while(link && ad->Status != HGS_IDLE)
        {
            team = link->data;
            sprintf(buf, "%d", team->Freq);
            void *dummy2 = HashGetOne(teams, buf);
            if(!dummy2)
            {
                chat->SendArenaMessage(a, "Team %d eliminated: no players left.", team->Freq);
                EliminateTeam(a, team->Freq);
            }
            link = link->next;
        }
        HashEnum(teams, hash_enum_afree, NULL);
        HashFree(teams);
        pthread_mutex_unlock(&globalmutex);
    }
}

local void CheckForGameOver(Arena *a)
{
    HG_CHECK(a);

    if(ad->Status == HGS_ACTIVE)
    {
        int remaining = RemainingTeams(a);
        if(remaining == 0)
        {
            EndGame(a, HGR_NONE_REMAINING);
        }
        else if(remaining == 1)
        {
            EndGame(a, HGR_NATURAL);
        }
    }
}

///////////////////////////////////////////////////////////////////////
// Callbacks
///////////////////////////////////////////////////////////////////////
local void PlayerAction(Player *p, int action, Arena *a)
{
    HG_CHECK(a);

    if(action == PA_ENTERARENA && ad->Status != HGS_IDLE)
    {
        int *status = amalloc(sizeof(int));
        *status = HGS_IDLE;
        pthread_mutex_lock(&globalmutex);
        HashReplace(ad->Players, p->name, status);
        pthread_mutex_unlock(&globalmutex);
    }
    else if(action == PA_LEAVEARENA && ad->Status == HGS_ACTIVE)
    {
        EliminateEmptyTeams(a);
        CheckForGameOver(a);
    }
}

local void ShipChange(Player *p, int ship, int freq)
{
    HG_CHECK(p->arena);

    if(ad->Status == HGS_PREGAME)
    {
        pthread_mutex_lock(&globalmutex);
        int *status = HashGetOne(ad->Players, p->name);
        if(!status)
        {
            status = amalloc(sizeof(int));
            if(ship == SHIP_SPEC)
            {
                *status = HGS_IDLE;
            }
            else
            {
                *status = HGS_PREGAME;
            }

            HashReplace(ad->Players, p->name, status);
        }
        pthread_mutex_unlock(&globalmutex);
    }
    else if(ad->Status == HGS_ACTIVE && ad->cfg_ElimOnSpec)
    {
        if(ship == SHIP_SPEC)
        {
            EliminatePlayer(p->arena, p);
        }
    }
}

local void FreqChange(Player *p, int freq)
{
    HG_CHECK(p->arena);

    if(ad->Status == HGS_ACTIVE)
    {
        EliminateEmptyTeams(p->arena);
        CheckForGameOver(p->arena);
    }
}

local void ArenaAction(Arena *a, int action)
{
    if(action == AA_DESTROY)
    {
        EndGame(a, HGR_FORCED);
        DetachGame(a);
    }
}

///////////////////////////////////////////////////////////////////////
// Timers
///////////////////////////////////////////////////////////////////////


///////////////////////////////////////////////////////////////////////
// Commands
///////////////////////////////////////////////////////////////////////
local void C_showteams(const char *command, const char *params, Player *p, const Target *target)
{
    HG_CHECK(p->arena);

    if(ad->Status != HGS_IDLE)
    {
        TeamEnum(p->arena, (HGTeamEnumFunc *)ShowTeam, p);
    }
    else
    {
        chat->SendMessage(p, "No game in progress.");
    }
}

local void C_addteam(const char *command, const char *params, Player *p, const Target *target)
{
    HG_CHECK(p->arena);
    if(!HostAllowed(p->arena, p)) return;

    if(target->type == T_FREQ)
    {
        if(ad->Status == HGS_PREGAME)
        {
            if(GetTeam(target->u.freq.arena, target->u.freq.freq))
            {
                chat->SendMessage(p, "That team is already in the game.");
            }
            else
            {
                AddTeam(target->u.freq.arena, target->u.freq.freq);
                chat->SendMessage(p, "Team added to the game.");
                HGTeam *team = GetTeam(target->u.freq.arena, target->u.freq.freq);
                team->Status = HGS_PREGAME;
            }
        }
        else
        {
            chat->SendMessage(p, "Can only add teams before a new game.");
        }
    }
}

local void C_removeteam(const char *command, const char *params, Player *p, const Target *target)
{
    HG_CHECK(p->arena);
    if(!HostAllowed(p->arena, p)) return;

    if(target->type == T_FREQ)
    {
        if(ad->Status == HGS_PREGAME)
        {
            if(GetTeam(target->u.freq.arena, target->u.freq.freq))
            {
                RemoveTeam(target->u.freq.arena, target->u.freq.freq);
                chat->SendMessage(p, "Team removed from the game.");
            }
            else
            {

                chat->SendMessage(p, "That team is not currently in the game.");
            }
        }
        else
        {
            chat->SendMessage(p, "Can only remove teams before a game starts.");
        }
    }
}

local void C_newgame(const char *command, const char *params, Player *p, const Target *target)
{
    HG_CHECK(p->arena);
    if(!HostAllowed(p->arena, p)) return;

    if(ad->Status == HGS_IDLE)
    {
        PrepareGame(p->arena, p, params);
    }
    else
    {
        chat->SendMessage(p, "Game already in progress.");
    }
}

local void C_startgame(const char *command, const char *params, Player *p, const Target *target)
{
    HG_CHECK(p->arena);
    if(!HostAllowed(p->arena, p)) return;

    if(ad->Status == HGS_PREGAME)
    {
        if(ad->cfg_AutoTeamSize > 0)
        {
            MakeAutoTeams(p->arena, ad->cfg_AutoTeamSize);
        }

        pthread_mutex_lock(&globalmutex);
        int teams = LLCount(ad->Teams);
        pthread_mutex_unlock(&globalmutex);

        if(teams < 2)
        {
            chat->SendMessage(p, "There must be at least 2 teams to start a game.");
        }
        else
        {
            StartGame(p->arena, p);
        }
    }
    else if(ad->Status == HGS_ACTIVE || ad->Status == HGS_POSTGAME)
    {
        chat->SendMessage(p, "Game already in progress.");
    }
    else
    {
        chat->SendMessage(p, "There is no game to start.");
    }
}

local void C_stopgame(const char *command, const char *params, Player *p, const Target *target)
{
    HG_CHECK(p->arena);
    if(!HostAllowed(p->arena, p)) return;

    if(ad->Status == HGS_PREGAME)
    {
        chat->SendMessage(p, "Game cancelled.");
        EndGame(p->arena, HGR_CANCELLED);
    }
    else if(ad->Status == HGS_ACTIVE || ad->Status == HGS_POSTGAME)
    {
        chat->SendArenaMessage(p->arena, "Game stopped by %s.", p->name);
        EndGame(p->arena, HGR_STOPPED);
    }
    else
    {
        chat->SendMessage(p, "There is no game in progress to stop");
    }
}

local void C_listgames(const char *command, const char *params, Player *p, const Target *target)
{
    hgcore_adata *ad = P_ARENA_DATA(p->arena, adkey);

    char game[32];
    const char *games = ad->cfg_AllowedGames;
    if(!games)
    {
        chat->SendMessage(p, "There are no allowed games for this arena.");
    }
    else
    {
        chat->SendMessage(p, "You may switch to the following games:");

        const char *tmp = NULL;
        char buf[40];

        while (strsplit(games, " \t:;,", game, sizeof(game), &tmp))
        {
            sprintf(buf, "host_%s", game);
            if(capman->HasCapability(p, buf))
            {
                chat->SendMessage(p, "%s", game);
            }
        }
    }
}

local void C_switchgame(const char *command, const char *params, Player *p, const Target *target)
{
    hgcore_adata *ad = P_ARENA_DATA(p->arena, adkey);

    if(strlen(params) > 0)
    {
        if(ad->Status == HGS_IDLE)
        {
            if(GameAllowed(p->arena, params))
            {
                char buf[40];
                sprintf(buf, "host_%s", params);
                if(capman->HasCapability(p, buf))
                {
                    if(ad->hg == NULL || mm->DetachModule(ad->AttachedGame, p->arena) == MM_OK)
                    {
                        if(mm->AttachModule(params, p->arena) == MM_OK)
                        {
                            if(ad->hg)
                            {
                                chat->SendMessage(p, "Game switched to %s.", params);
                            }
                            else
                            {
                                chat->SendMessage(p, "Game module attached, but failed to set as current game.");
                            }
                        }
                        else
                        {
                            chat->SendMessage(p, "Could not attach game %s.", params);
                        }
                    }
                    else
                    {
                        chat->SendMessage(p, "Could not detach current game %s.", ad->AttachedGame);
                    }
                }
                else
                {
                    chat->SendMessage(p, "You do not have sufficient permissions to switch to game %s.", params);
                }
            }
            else
            {
                chat->SendMessage(p, "Game %s is not allowed in this arena.", params);
            }
        }
        else
        {
            chat->SendMessage(p, "There is currently a game in progress.");
        }
    }
    else
    {
        chat->SendMessage(p, "You must specify a game to switch to.");
    }
}
